<!DOCTYPE html>
<meta charset=utf-8>
<title>重构 - 深入 Python 3</title><!--[if IE]><script src=j/html5.js></script><![endif]-->

<link rel=stylesheet href="dip3.css">
<style>
body{counter-reset:h1 10}
</style>
<link rel=stylesheet media='only screen and (max-device-width: 480px)' href="http://woodpecker.org.cn/diveintopython3/mobile.css">
<link rel=stylesheet media=print href="http://woodpecker.org.cn/diveintopython3/print.css">
<meta name=viewport content='initial-scale=1.0'>
<form action=http://www.google.com/cse><div><input type=hidden name=cx value=014021643941856155761:l5eihuescdw><input type=hidden name=ie value=UTF-8>&nbsp;<input type=search name=q size=25 placeholder="powered by Google&trade;">&nbsp;<input type=submit name=root value=搜索></div></form>
<p>当前位置: <a href="index.html">首页</a> <span class=u>‣</span> <a href="table-of-contents.html#refactoring">深入 Python 3</a> <span class=u>‣</span>
<p id=level>难度级别： <span class=u title=advanced>♦♦♦♦♢</span>

<h1>重构</h1>
<blockquote class=q>
<p><span class=u>❝</span> After one has played a vast quantity of notes and more notes, it is simplicity that emerges as the crowning reward of art. <span class=u>❞</span><br>— <a href=http://en.wikiquote.org/wiki/Fr%C3%A9d%C3%A9ric_Chopin>Frédéric Chopin</a>
</blockquote>
<p id=toc>&nbsp;
<h2 id=divingin>深入</h2>
<p class=f>就算是竭尽了全力编写全面的单元测试，还是会遇到错误。我所说的“错误”是什么意思？错误是尚未写到的测试实例。<pre class=screen><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>import roman7</kbd>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>roman7.from_roman('')</kbd> <span class=u>①</span></a>
<samp class=pp>0</samp></pre>
<ol>
<li>这就是错误。和其它无效罗马数字的一系列字符一样，空字符串将引发 <code>InvalidRomanNumeralError</code> 例外。</li></ol>

<p>在重现该错误后，应该在修复前写出一个导致该失败情形的测试实例，这样才能描述该错误。<pre class=pp><code>class FromRomanBadInput(unittest.TestCase):  
    .
    .
    .
    def testBlank(self):
        '''from_roman should fail with blank string'''
<a>        self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') <span class=u>①</span></a></code></pre>
<ol>
<li>这段代码非常简单。通过传入一个空字符串调用 <code>from_roman()</code> ，并确保其引发一个 <code>InvalidRomanNumeralError</code> 例外。难的是发现错误；找到了该错误之后对它进行测试是件轻松的工作。</li></ol>

<p>由于代码有错误，且有用于测试该错误的测试实例，该测试实例将会导致失败：<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest8.py -v</kbd>
<samp>from_roman should fail with blank string ... FAIL
from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
FAIL: from_roman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest8.py", line 117, in test_blank
    self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
<mark>AssertionError: InvalidRomanNumeralError not raised by from_roman</mark>

----------------------------------------------------------------------
Ran 11 tests in 0.171s

FAILED (failures=1)</samp></pre>

<p><em>现在</em> 可以修复该错误了。<pre class=pp><code>def from_roman(s):
    '''convert Roman numeral to integer'''
<a>    if not s:                                                                  <span class=u>①</span></a>
        raise InvalidRomanNumeralError('Input can not be blank')
    if not re.search(romanNumeralPattern, s):
<a>        raise InvalidRomanNumeralError('Invalid Roman numeral: {}'.format(s))  <span class=u>②</span></a>

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result</code></pre>
<ol>
<li>只需两行代码：一行明确地对空字符串进行检查，另一行为 <code>raise</code> 语句。</li><li>在本书中还尚未提到该内容，因此现在让我们讲讲 <a href="strings.html#formatting-strings">字符串格式化</a> 最后一点内容。从 Python 3.1 起，在格式化标示符中使用位置索引时可以忽略数字。也就是说，无需使用格式化标示符 <code>{0}</code> 来指向 <code>format()</code> 方法的第一个参数，只需简单地使用 <code>{}</code> 而 Python 将会填入正确的位置索引。该规则适用于任何数量的参数；第一个 <code>{}</code> 代表 <code>{0}</code>，第二个 <code>{}</code> 代表 <code>{1}</code>，以此类推。</li></ol>

<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest8.py -v</kbd>
<a><samp>from_roman should fail with blank string ... ok</samp>  <span class=u>①</span></a>
<samp>from_roman should fail with malformed antecedents ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 11 tests in 0.156s
</samp>
<a><samp>OK</samp>  <span class=u>②</span></a></pre>
<ol>
<li>现在空字符串测试实例通过了测试，也就是说错误被修正了。</li><li>所有其它测试实例仍然可以通过，说明该错误修正没有破坏其它部分。代码编写结束。</li></ol>

<p>用此方式编写代码将使得错误修正变得更困难。简单的错误（像这个）需要简单的测试实例；复杂的错误将会需要复杂的测试实例。在以测试为中心的环境中，由于必须在代码中精确地描述错误（编写测试实例），然后修正错误本身，看起来 <em>好像</em> 修正错误需要更多的时间。而如果测试实例无法正确地通过，则又需要找出到底是修正方案有错误，还数测试实例本身就有错误。然而从长远看，这种在测试代码和经测试代码之间的来回折腾是值得的，因为这样才更有可能在第一时间修正错误。同时，由于可以对新代码轻松地重新运行 <em>所有</em> 测试实例，在修正新代码时破坏旧代码的机会更低。今天的单元测试就是明天的回归测试。<p class=a>⁂

<h2 id=changing-requirements>控制需求变化</h2>
<p>为了获取准确的需求，尽管已经竭力将客户“钉”在原地，并经历了反复剪切、粘贴的痛苦，但需求仍然会变化。大多数客户在看到产品之前不知道自己想要什么，而且就算知道，他们也不擅长清晰地表述自己的想法。而即便擅长表述，他们在下一个版本中也会提出更多要求。因此，必须随时准备好更新测试实例以应对需求变化。<p>举个例子来说，假定我们要扩展罗马数字转换函数的能力范围。正常情况下，罗马数字中的任何一个字符在同一行中不得重复出现三次以上。但罗马人却愿意该规则有个例外：通过一行中的 4 个 <code>M</code> 字符来代表 <code>4000</code> 。进行该修改后，将会把可转换数字的范围从 <code>1..3999</code> 拓展为 <code>1..4999</code>。但首先必须对测试实例进行一些修改。<p class=d>[<a href="examples/roman8.py">download <code>roman8.py</code></a>]
<pre class=pp><code>class KnownValues(unittest.TestCase):
    known_values = ( (1, 'I'),
                      .
                      .
                      .
                     (3999, 'MMMCMXCIX'),
<a>                     (4000, 'MMMM'),                                      <span class=u>①</span></a>
                     (4500, 'MMMMD'),
                     (4888, 'MMMMDCCCLXXXVIII'),
                     (4999, 'MMMMCMXCIX') )

class ToRomanBadInput(unittest.TestCase):
    def test_too_large(self):
        '''to_roman should fail with large input'''
<a>        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 5000)  <span class=u>②</span></a>

    .
    .
    .

class FromRomanBadInput(unittest.TestCase):
    def test_too_many_repeated_numerals(self):
        '''from_roman should fail with too many repeated numerals'''
<a>        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):     <span class=u>③</span></a>
            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)

    .
    .
    .

class RoundtripCheck(unittest.TestCase):
    def test_roundtrip(self):
        '''from_roman(to_roman(n))==n for all n'''
<a>        for integer in range(1, 5000):                                    <span class=u>④</span></a>
            numeral = roman8.to_roman(integer)
            result = roman8.from_roman(numeral)
            self.assertEqual(integer, result)</code></pre>
<ol>
<li>现有的已知数值不会变（它们依然是合理的测试数值），但必须在 <code>4000</code> 范围之内（外）增加一些。在此，我已经添加了 <code>4000</code> (最短)、 <code>4500</code> (第二短)、 <code>4888</code> (最长) 和 <code>4999</code> (最大)。</li><li>“过大值输入” 的定义已经发生了变化。该测试用于通过传入 <code>4000</code> 调用 <code>to_roman()</code> 并期望引发一个错误；目前 <code>4000-4999</code> 是有效的值，必须将该值调整为 <code>5000</code> 。</li><li>“太多重复数字”的定义也发生了变化。该测试通过传入 <code>'MMMM'</code> 调用 <code>from_roman()</code> 并预期发生一个错误；目前 <code>MMMM</code> 被认定为有效的罗马数字，必须将该条件修改为 <code>'MMMMM'</code> 。</li><li>对范围内的每个数字进行完整循环测试，从 <code>1</code> 到 <code>3999</code>。由于范围已经进行了拓展，该 <code>for</code> 循环同样需要修改为以 <code>4999</code> 为上限。</li></ol>

<p>现在，测试实例已经按照新的需求进行了更新，但代码还没有，因按照预期，某些测试实例将返回失败结果。<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
<a>from_roman should give known result with known input ... ERROR          <span class=u>①</span></a>
<a>to_roman should give known result with known input ... ERROR            <span class=u>②</span></a>
<a>from_roman(to_roman(n))==n for all n ... ERROR                          <span class=u>③</span></a>
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

======================================================================
ERROR: from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 82, in test_from_roman_known_values
    result = roman9.from_roman(numeral)
  File "C:\home\diveintopython3\examples\roman9.py", line 60, in from_roman
    raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
<mark>roman9.InvalidRomanNumeralError: Invalid Roman numeral: MMMM</mark>

======================================================================
ERROR: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 76, in test_to_roman_known_values
    result = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
<mark>roman9.OutOfRangeError: number out of range (must be 0..3999)</mark>

======================================================================
ERROR: from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 131, in testSanity
    numeral = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('number out of range (must be 0..3999)')
<mark>roman9.OutOfRangeError: number out of range (must be 0..3999)</mark>

----------------------------------------------------------------------
Ran 12 tests in 0.171s

FAILED (errors=3)</samp></pre>
<ol>
<li>一旦遇到 <code>'MMMM'</code>，<code>from_roman()</code> 已知值测试将会失败，因为 <code>from_roman()</code> 仍将其视为无效罗马数字。</li><li>一旦遇到 <code>4000</code>，<code>to_roman()</code> 已知值测试将会失败，因为 <code>to_roman()</code> 仍将其视为超范围数字。</li><li>而往返（译注：指在普通数字和罗马数字之间来回转换）检查遇到 <code>4000</code> 时也会失败，因为 <code>to_roman()</code> 仍认为其超范围。</li></ol>

<p>现在，我们有了一些由新需求导致失败的测试实例，可以考虑修正代码让它与新测试实例一致起来。（刚开始编写单元测试的时候，被测试代码绝不会在测试实例“之前”出现确实让人感觉有点怪。）尽管编码工作被置后安排，但还是不少要做的事情，一旦与测试实例相符，编码工作就可以结束了。一旦习惯单元测试后，您可能会对自己曾在编程时不进行测试感到很奇怪。）<p class=d>[<a href="examples/roman9.py">download <code>roman9.py</code></a>]
<pre class=pp><code>roman_numeral_pattern = re.compile('''
    ^                   # beginning of string
<a>    M{0,4}              # thousands - 0 to 4 Ms  <span class=u>①</span></a>
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    ''', re.VERBOSE)

def to_roman(n):
    '''convert integer to Roman numeral'''
<a>    if not (0 &lt; n &lt; 5000):                        <span class=u>②</span></a>
        raise OutOfRangeError('number out of range (must be 1..4999)')
    if not isinstance(n, int):
        raise NotIntegerError('non-integers can not be converted')

    result = ''
    for numeral, integer in roman_numeral_map:
        while n &gt;= integer:
            result += numeral
            n -= integer
    return result

def from_roman(s):
    .
    .
    .</code></pre>
<ol>
<li>根本无需对 <code>from_roman()</code> 函数进行任何修改。唯一需要修改的是 <var>roman_numeral_pattern</var> 。仔细观察下，将会发现我已经在正则表达式的第一部分中将 <code>M</code> 字符的数量从 <code>3</code> 优化为 <code>4</code> 。该修改将允许等价于 <code>4999</code> 而不是 <code>3999</code> 的罗马数字。实际的 <code>from_roman()</code> 函数完全是通用的；它只查找重复的罗马数字字符并将它们加起来，而不关心它们重复了多少次。之前无法处理 <code>'MMMM'</code> 的唯一原因是我们通过正则表达式匹配明确地阻止了它这么做。</li><li><code>to_roman()</code> 函数只需在范围检查中进行一个小改动。将之前检查 <code>0 &lt; n &lt; 4000</code> 的地方现在修改为检查 <code>0 &lt; n &lt; 5000</code> 。同时修改 <code>引发</code> 的错误信息，以体现新的可接受范围 (<code>1..4999</code> 取代 <code>1..3999</code>) 。无需对函数剩下部分进行任何修改；它已经能够应对新的实例。（它将对找到的每个千位增加 <code>'M'</code> ；如果给定 <code>4000</code>，它将给出 <code>'MMMM'</code>。之前它不这么做的唯一原因是我们通过范围检查明确地阻止了它。）</li></ol>

<p>所需做的就是这两处小修改，但你可能会有点怀疑。嗨，别光听我说，你自己看看吧。<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.203s

<a>OK  <span class=u>①</span></a></samp></pre>
<ol>
<li>所有测试实例均通过了。代码编写结束。</li></ol>

<p>全面单元测试的意思是：无需依赖某个程序员来说“相信我吧。”<p class=a>⁂

<h2 id=refactoring>重构</h2>

<p>关于全面单元测试，最美妙的事情不是在所有的测试实例通过后的那份心情，也不是别人抱怨你破坏了代码，而你通过实践 <em>证明</em> 自己没有时的快感。单元测试最美妙之处在于它给了你大刀阔斧进行重构的自由。<p>重构是修改可运作代码，使其表现更佳的过程。通常，“更佳”指的是“更快”，但它也可能指的是“占用更少内存“、”占用更少磁盘空间“或者”更加简洁”。对于你的环境、你的项目来说，无论重构意味着什么，它对程序的长期健康都至关重要。<p>本例中，“更佳”的意思既包括“更快”也包括“更易于维护”。具体而言，因为用于验证罗马数字的正则表达式生涩冗长，该 <code>from_roman()</code> 函数比我所希望的更慢，也更加复杂。现在，你可能会想，“当然，正则表达式就又臭又长的，难道我有其它办法验证任意字符串是否为罗马数字吗？”<p>答案是：只针对 5000 个数进行转换；为什么不知建立一个查询表呢？意识到 <em>根本不需要使用正则表达式</em> 之后，这个主意甚至变得更加理想了。在建立将整数转换为罗马数字的查询表的同时，还可以建立将罗马数字转换为整数的逆向查询表。在需要检查任意字符串是否是有效罗马数字的时候，你将收集到所有有效的罗马数字。“验证”工作简化为一个简单的字典查询。<p>最棒的是，你已经有了一整套单元测试。可以修改模块中一半以上的代码，而单元测试将会保持不变。这意味着可以向你和其他人证明：新代码运作和最初的一样好。<p class=d>[<a href="examples/roman10.py">download <code>roman10.py</code></a>]
<pre class=pp><code>class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
class InvalidRomanNumeralError(ValueError): pass

roman_numeral_map = (('M',  1000),
                     ('CM', 900),
                     ('D',  500),
                     ('CD', 400),
                     ('C',  100),
                     ('XC', 90),
                     ('L',  50),
                     ('XL', 40),
                     ('X',  10),
                     ('IX', 9),
                     ('V',  5),
                     ('IV', 4),
                     ('I',  1))

to_roman_table = [ None ]
from_roman_table = {}

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 &lt; n &lt; 5000):
        raise OutOfRangeError('number out of range (must be 1..4999)')
    if int(n) != n:
        raise NotIntegerError('non-integers can not be converted')
    return to_roman_table[n]

def from_roman(s):
    '''convert Roman numeral to integer'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('Input must be a string')
    if not s:
        raise InvalidRomanNumeralError('Input can not be blank')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
    return from_roman_table[s]

def build_lookup_tables():
    def to_roman(n):
        result = ''
        for numeral, integer in roman_numeral_map:
            if n &gt;= integer:
                result = numeral
                n -= integer
                break
        if n &gt; 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
        roman_numeral = to_roman(integer)
        to_roman_table.append(roman_numeral)
        from_roman_table[roman_numeral] = integer

build_lookup_tables()</code></pre>

<p>让我们打断一下，进行一些剖析工作。可以说，最重要的是最后一行：<pre class='nd pp'><code>build_lookup_tables()</code></pre>

<p>可以注意到这是一次函数调用，但没有 <code>if</code> 语句包裹住它。这不是 <code>if __name__ == '__main__'</code> 语块；<em>模块被导入时</em> 它将会被调用。（重要的是必须明白：模块将只被导入一次，随后被缓存了。如果导入一个已导入模块，将不会导致任何事情发生。因此这段代码将只在第一此导入时运行。）<p>那么，该 <code>build_lookup_tables()</code> 函数究竟进行了哪些操作呢?很高兴你问这个问题。<pre class=pp><code>to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
<a>    def to_roman(n):                                <span class=u>①</span></a>
        result = ''
        for numeral, integer in roman_numeral_map:
            if n &gt;= integer:
                result = numeral
                n -= integer
                break
        if n &gt; 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
<a>        roman_numeral = to_roman(integer)          <span class=u>②</span></a>
<a>        to_roman_table.append(roman_numeral)       <span class=u>③</span></a>
        from_roman_table[roman_numeral] = integer</code></pre>
<ol>
<li>这是一段聪明的程序代码……也许过于聪明了。上面定义了 <code>to_roman()</code> 函数；它在查询表中查找值并返回结果。而 <code>build_lookup_tables()</code> 函数重定义了 <code>to_roman()</code> 函数用于实际操作（像添加查询表之前的例子一样）。在 <code>build_lookup_tables()</code> 函数内部，对 <code>to_roman()</code> 的调用将会针对该重定义的版本。一旦 <code>build_lookup_tables()</code> 函数退出，重定义的版本将会消失&nbsp;—&nbsp;它的定义只在 <code>build_lookup_tables()</code> 函数的作用域内生效。</li><li>该行代码将调用重定义的 <code>to_roman()</code> 函数，该函数实际计算罗马数字。</li><li>一旦获得结果（从重定义的 <code>to_roman()</code> 函数），可将整数及其对应的罗马数字添加到两个查询表中。</li></ol>

<p>查询表建好后，剩下的代码既容易又快捷。<pre class=pp><code>def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 &lt; n &lt; 5000):
        raise OutOfRangeError('number out of range (must be 1..4999)')
    if int(n) != n:
        raise NotIntegerError('non-integers can not be converted')
<a>    return to_roman_table[n]                                            <span class=u>①</span></a>

def from_roman(s):
    '''convert Roman numeral to integer'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('Input must be a string')
    if not s:
        raise InvalidRomanNumeralError('Input can not be blank')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
<a>    return from_roman_table[s]                                          <span class=u>②</span></a></code></pre>
<ol>
<li>像前面那样进行同样的边界检查之后，<code>to_roman()</code> 函数只需在查询表中查找并返回适当的值。</li><li>同样，<code>from_roman()</code> 函数也缩水为一些边界检查和一行代码。不再有正则表达式。不再有循环。O(1) 转换为或转换到罗马数字。</li></ol>

<p>但这段代码可以运作吗？为什么可以，是的它可以。而且我可以证明。<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/examples$ </samp><kbd>python3 romantest10.py -v</kbd>
<samp>from_roman should fail with blank string ... ok
from_roman should fail with malformed antecedents ... ok
from_roman should fail with non-string input ... ok
from_roman should fail with repeated pairs of numerals ... ok
from_roman should fail with too many repeated numerals ... ok
from_roman should give known result with known input ... ok
to_roman should give known result with known input ... ok
from_roman(to_roman(n))==n for all n ... ok
to_roman should fail with negative input ... ok
to_roman should fail with non-integer input ... ok
to_roman should fail with large input ... ok
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
<a>Ran 12 tests in 0.031s                                                  <span class=u>①</span></a>

OK</samp></pre>
<ol>
<li>它不仅能够回答你的问题，还运行得非常快！好象速度提升了 10 倍。当然，这种比较并不公平，因为此版本在导入时耗时更长（在建造查询表时）。但由于只进行一次导入，启动的成本可以由对 <code>to_roman()</code> 和 <code>from_roman()</code> 函数的所有调用摊薄。由于该测试进行几千次函数调用（来回单独测试上万次），节省出来的效率成本得以迅速提升！</li></ol>

<p>这个故事的寓意是什么？<ul>
<li>简单是一种美德。</li><li>特别在涉及到正则表达式的时候。</li><li>单元测试令你在进行大规模重构时充满自信。</li></ul>

<p class=a>⁂

<h2 id=summary>摘要</h2>

<p>单元测试是一个威力强大的概念，如果正确实施，不但可以降低维护成本，还可以提高长期项目的灵活性。但同时还必须明白：单元测试既不是灵丹妙药，也不是解决问题的魔术，更不是银弹。编写良好的测试实例非常艰难，确保它们时刻保持最新必须成为一项纪律（特别在客户要求关键错误修正时）。单元测试不是功能测试、集成测试或用户承受能力测试等其它测试的替代品。但它是可行的、行之有效的，见识过其功用后，你将对之前曾没有用它而感到奇怪。<p>这几章覆盖的内容很多，很大一部分都不是 Python 所特有的。许多语言都有单元测试框架，但所有框架都要求掌握同一基本概念：<ul>
<li>设计测试实例是件具体、自动且独立的工作。</li><li>在编写被测试代码 <em>之前</em> 编写测试实例。</li><li>编写用于检查好输入并验证正确结果的测试</li><li>编写用于测试“坏”输入并做出正确失败响应的测试。</li><li>编写并更新测试实例以反映新的需求</li><li>毫不留情地重构以提升性能、可扩展性、可读性、可维护性及任何缺乏的特性。</li></ul>

<p class=v><a rel=prev href="unit-testing.html" title="back to &#8220;Unit Testing&#8221;"><span class=u>☜</span></a> <a href="files.html" rel=next title="onward to &#8220;Files&#8221;"><span class=u>☞</span></a>
<p class=c>© 2001–9 <a href="about.html">Mark Pilgrim</a>
<script src="j/jquery.js"></script>
<script src="j/prettify.js"></script>
<script src="j/dip3.js"></script>
